package com.pingan.myBatis.crypt.plugin;

import com.pingan.myBatis.crypt.annotations.CryptField;
import com.pingan.myBatis.crypt.handler.BeanHandler;
import com.pingan.myBatis.crypt.handler.ListHandler;
import com.pingan.myBatis.crypt.handler.StringHandler;
import com.pingan.myBatis.crypt.utils.IsNotCryptUtil;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.defaults.DefaultSqlSession;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @AUTHOR:LIUCHAO;  定义拦截器 来拦截增删改查
 * @DATE: 2020/11/30 16:10
 */

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class,
                        BoundSql.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
@Component
public class CryptInterceptor implements Interceptor {
//    mvn install:install-file -Dfile=E:\java_project\data-structure\spring_mybatis_encode\myBatis\target\myBatis-1.0-SNAPSHOT.jar -DgroupId=com.pingan -DartifactId=myBatis -Dversion=1.0-SNAPSHOT -Dpackaging=jar -DgeneratePom=true
    private static final String PARAM = "param";

    private static final String MAPPEDSTATEMENT_ID_SEPERATOR = ".";

    private static final String PARAM_TYPE_LIST = "list";

    private static final String PARAM_TYPE_COLLECTION = "collection";
    /**
     * 适用于解密判断
     */
    private static final ConcurrentHashMap<String, Boolean> METHOD_ANNOTATIONS_MAP = new ConcurrentHashMap<>();

    /**
     * 适用于加密判断
     */
    private static final ConcurrentHashMap<String, Set<String>> METHOD_PARAM_ANNOTATIONS_MAP = new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        // 入参
        Object parameter = args[1];
        MappedStatement statement = (MappedStatement) args[0];
        // 判断是否需要加密  取反，表示double  int  long  boolean 不加密
        if (!IsNotCryptUtil.isNotCrypt(parameter)) {
            // 单参数 string
            if (parameter instanceof String) {

                args[1] = StringHandler.stringEncrypt((String) parameter, getParameterAnnotations(statement));
                // 单参数 list
            } else if (parameter instanceof DefaultSqlSession.StrictMap) {
                DefaultSqlSession.StrictMap<Object> strictMap = (DefaultSqlSession.StrictMap<Object>) parameter;
                for (Map.Entry<String, Object> entry : strictMap.entrySet()) {
                    if (entry.getKey().contains(PARAM_TYPE_COLLECTION)) {
                        continue;
                    }
                    if (entry.getKey().contains(PARAM_TYPE_LIST)) {
                        Set<String> set = getParameterAnnotations(statement);
                        ListHandler.listEncrypt((List) entry.getValue(), !set.isEmpty());
                    }
                }
                // 多参数
            } else if (parameter instanceof MapperMethod.ParamMap) {
                MapperMethod.ParamMap<Object> paramMap = (MapperMethod.ParamMap<Object>) parameter;
                Set<String> set = getParameterAnnotations(statement);
                boolean setEmpty = set.isEmpty();
                // 解析每一个参数
                for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
                    // 判断不需要解析的类型 不解析map
                    if (IsNotCryptUtil.isNotCrypt(entry.getValue()) || entry.getValue() instanceof Map || entry.getKey().contains(PARAM)) {
                        continue;
                    }
                    // 如果string
                    if (entry.getValue() instanceof String) {
                        entry.setValue(StringHandler.stringEncrypt(entry.getKey(), (String) entry.getValue(), set));
                        continue;
                    }
                    boolean isSetValue = !setEmpty && set.contains(entry.getKey());
                    // 如果 list
                    if (entry.getValue() instanceof List) {
                        ListHandler.listEncrypt((List) entry.getValue(), isSetValue);
                        continue;
                    }
                    BeanHandler.beanEncrypt(entry.getValue());
                }
                // bean
            } else {
                BeanHandler.beanEncrypt(parameter);
            }
        }

        // 获得出参
        Object returnValue = invocation.proceed();

        // 出参解密
        if (IsNotCryptUtil.isNotCrypt(returnValue)) {
            return returnValue;
        }
        Boolean bo = getMethodAnnotations(statement);
        if (returnValue instanceof String && bo) {
            return StringHandler.stringDecrypt((String) returnValue);
        }
        if (returnValue instanceof List) {
            ListHandler.listDecrypt((List) returnValue, bo);
            return returnValue;
        }

        return returnValue;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }





    /**
     * 获取 方法参数上的注解   查看该方法上是否有CryptField注解
     *   获取 有加密注解的方法
     * @param statement
     * @return
     * @throws ClassNotFoundException
     */
    private Set<String> getParameterAnnotations(MappedStatement statement) throws ClassNotFoundException {
        final String id = statement.getId();//方法的全路径名
        Set<String> set = METHOD_PARAM_ANNOTATIONS_MAP.get(id);
        if (set != null) {
            return set;
        }
        set = new HashSet<>();
        //获取方法名
        Method m = getMethodByMappedStatementId(id);
        if (m == null) {
            return set;
        }
        final Annotation[][] paramAnnotations = m.getParameterAnnotations();
        // get names from @CryptField annotations
        for (Annotation[] paramAnnotation : paramAnnotations) {
            for (Annotation annotation : paramAnnotation) {
                if (annotation instanceof CryptField) {
                    CryptField cryptField = (CryptField) annotation;
                    // 如果允许加密   有该注解，将拥有该注解的方法出入set中，最后传入
                    if (cryptField.encrypt()) {
                        set.add(cryptField.value());
                    }
                    break;
                }
            }
        }

        Set<String> oldSet = METHOD_PARAM_ANNOTATIONS_MAP.putIfAbsent(id, set);
        if (oldSet != null) {
            set = oldSet;
        }

        return set;
    }

    /**
     * 通过mappedStatementId get Method
     *
     * @param id
     * @return
     * @throws ClassNotFoundException
     */
    private Method getMethodByMappedStatementId(String id) throws ClassNotFoundException {
        Method m = null;
        final Class clazz = Class.forName(id.substring(0, id.lastIndexOf(MAPPEDSTATEMENT_ID_SEPERATOR)));
        for (Method method : clazz.getMethods()) {
            if (method.getName().equals(id.substring(id.lastIndexOf(MAPPEDSTATEMENT_ID_SEPERATOR) + 1))) {
                m = method;
                break;
            }
        }

        return m;
    }

    /**
     * 获取 方法上的注解
     *
     * @param statement
     * @return
     * @throws ClassNotFoundException
     */
    private Boolean getMethodAnnotations(MappedStatement statement) throws ClassNotFoundException {
        final String id = statement.getId();
        Boolean bo = METHOD_ANNOTATIONS_MAP.get(id);
        if (bo != null) {
            return bo;
        }
        //获取方法
        Method m = getMethodByMappedStatementId(id);
        if (m == null) {
            return Boolean.FALSE;
        }
        final CryptField cryptField = m.getAnnotation(CryptField.class);
        // 如果允许解密
        if (cryptField != null && cryptField.decrypt()) {
            bo = Boolean.TRUE;
        } else {
            bo = Boolean.FALSE;
        }
        Boolean bo1 = METHOD_ANNOTATIONS_MAP.putIfAbsent(id, bo);
        if (bo1 != null) {
            bo = bo1;
        }

        return bo;
    }

}
